﻿/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.server.functions;

import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.dataholders.MessageHandler;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.utils.Vector;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.stars.Concentrations;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.server.functions.GroundCombat;

typedef LoadTask = {
    var fleet : Fleet;
    var location : Vector;
    var fuel : CargoAmount;
    var colonists : CargoAmount;
    var minerals : Hash<CargoAmount>;
    var target : CargoTarget;
    var cargoCapacity : Int;
    var fuelCapacity : Int;
    //this is used to group them by location
    var found : Bool;
}

/**
    Performs all tasks dealing with cargo (loading, unloading, etc).
**/
class CargoTasks {
    
    /**
        Loops through all of the fleets in [Global.fleets] and performs the 
        loading on fleets that have a waypoint 0 order of [Task.Load].
        If the intended target no longer exists or is no longer at that 
        location, the [waypoint0] is set to [NoTask].
    **/
    public static function doLoadTasks() {
        //TODO: HA! To allow for looking at only 1 enum, just simply use the 
        //default case, so the hacks I've used need to be fixed.
        
        var loadTasks = new Array<LoadTask>();
        
        for (f in Global.fleets) {
            
            switch(f.waypoint0) {
                
                case /*Task.*/Load(fuel, minerals, colonists, ct):
                    loadTasks.push( { fleet : f, location : f.location,
                            fuel : fuel, minerals : minerals, target : ct,
                            colonists : colonists, 
                            cargoCapacity : f.getCargoCapacity(),
                            fuelCapacity : Std.int(f.getFuelCapacity()),
                            found : false } );
                
                default:
                    //do nothing
            }
        }
        
        //update their targets (if the target is null or no longer at this 
        //location, set the task to no task and remove it.
        var i = loadTasks.length;
        while (i-- > 0) {
            var t = loadTasks[i];
            
            var update = updateCargoTarget(t.target);
            if (update.isNull || 
                    !checkTargetLocation(update.ct, t.fleet.location)) {
                t.fleet.waypoint0 = NoTask;
                loadTasks.splice(i, 1);
            }
        }
        
        //group them by location
        var locations = new Array < Array < LoadTask >> ();
        for (task in loadTasks) {
            if (task.found) continue;
            
            task.found = true;
            var sameLocation = new Array < LoadTask > ();
            sameLocation.push(task);
            
            for (otherTask in loadTasks) {
                if (otherTask == task) continue;
                
                if (otherTask.location.equals(task.location)) {
                    otherTask.found = true;
                    sameLocation.push(otherTask);
                }
            }
            locations.push(sameLocation);
        }
        
        //loop through, resolve conflicts while doing so
        //Order: 
        //load from salvage
        //load stars
        //load from fleets of the same player.
        
        for (location in locations) {
            //set found to fals
            for (task in location) task.found = false;
            
            doSalvageLoads(location);
            doStarLoads(location);
            doFleetLoads(location);
            
            //set the waypoint 0 to no task.
            
            for (task in location) task.fleet.waypoint0 = NoTask;
        }
            
        
    }
    
    private static function doFleetLoads(tasks : Array < LoadTask > ) {
        for (task in tasks) {
            if (task.found) continue;
            
            switch(task.target) {
                
                case /*CargoTarget.*/FleetCargo(f):
                    //find other fleet targets
                    var fleetTasks = new Array<LoadTask>();
                    fleetTasks.push(task);
                    task.found = true;
                        
                    for (otherTask in tasks) {
                        if (otherTask.found) continue;
                        
                        switch(otherTask.target) {
                            case /*CargoTarget.*/FleetCargo(f2):
                                if (f.id == f2.id) {
                                    otherTask.found = true;
                                    fleetTasks.push(otherTask);
                                }
                            
                            default:
                                    //do nothing
                        }
                    }
                    
                    if(f.owner.id != Global.players.nullPlayer.id) {
                        //check to make sure they're loading from a fleet they own
                        var i = fleetTasks.length;
                        while (i-- > 0) {
                            var t = fleetTasks[i];
                            if (t.fleet.owner.id != f.owner.id) {
                                //TODO: add message here
                                //Global.messages.sendMessage( 
                                //  { to : Target.PlayerTarget(t.fleet.owner), 
                                //  from : Target.Host, 
                                //  data : MessageData.FailedLoadFromStar(s.name)
                                //  } );
                                
                                fleetTasks.splice(i, 1);
                            }
                        }
                    }
                    
                    var leftovers = resolveLoading(fleetTasks, f.fuel, 
                            f.minerals, f.colonists);
                    f.fuel = leftovers.fuel;
                    f.colonists = f.colonists;
                 
                default:
                    //do nothing
            }
        }
    }
    
    private static function doStarLoads(tasks : Array < LoadTask > ) {
        for (task in tasks) {
            if (task.found) continue;
            
            switch(task.target) {
                
                case /*CargoTarget.*/StarCargo(s):
                    //find other star targets
                    var starTasks = new Array<LoadTask>();
                    starTasks.push(task);
                    task.found = true;
                        
                    for (otherTask in tasks) {
                        if (otherTask.found) continue;
                        
                        switch(otherTask.target) {
                            case /*CargoTarget.*/StarCargo(s2):
                                if (s.id == s2.id) {
                                    otherTask.found = true;
                                    starTasks.push(otherTask);
                                }
                            
                            default:
                                    //do nothing
                        }
                    }
                    
                    if(s.owner.id != Global.players.nullPlayer.id) {
                        //check to make sure they're loading from a star they own
                        var i = starTasks.length;
                        while (i-- > 0) {
                            var t = starTasks[i];
                            if (t.fleet.owner.id != s.owner.id) {
                                Global.messages.sendMessage( 
                                  { to : Target.PlayerTarget(t.fleet.owner), 
                                  from : Target.Host, 
                                  data : MessageData.FailedLoadFromStar(s.name)
                                  } );
                                
                                starTasks.splice(i, 1);
                            }
                        }
                        
                        s.population = resolveLoading(starTasks, 0, 
                                s.minerals, s.population).colonists;
                        //check to see if the star is uninhabited.
                        s.checkPopulation();
                    } else {
                        resolveLoading(starTasks, 0, s.minerals, 0);
                    }
                 
                default:
                    //do nothing
            }
        }
    }
    
    private static function doSalvageLoads(tasks : Array < LoadTask > ) {
        for (task in tasks) {
            if (task.found) continue;
            
            switch(task.target) {
                
                case SalvageCargo(s):
                    //find other salvage targets
                    var salvageTasks = new Array<LoadTask>();
                    salvageTasks.push(task);
                    task.found = true;
                        
                    for (otherTask in tasks) {
                        if (otherTask.found) continue;
                        
                        switch(otherTask.target) {
                            case SalvageCargo(s2):
                                if (s.id == s2.id) {
                                    otherTask.found = true;
                                    salvageTasks.push(otherTask);
                                }
                            
                            default:
                                    //do nothing
                        }
                    }
                    
                    resolveLoading(salvageTasks, 0, s.minerals, 0);
                 
                default:
                    //do nothing
            }
        }
    }
    
    //returns the fuel and colonists leftover so that the target fuel can be 
    //set by the caller (since Floats/Ints are passed by value).
    private static function resolveLoading(tasks : Array<LoadTask>, 
            fuel : Float, minerals : MineralSet, colonists : Int) 
            : {fuel : Float, colonists : Int} {
        
        //make copies of all the cargo data so that when a fleet tries to load 
        //a percent of cargo, its taken from the original amounts, not the
        //amount after other fleets have taken some.
        //NOTE: Fuel is stored separately from colonists and minerals
        
        var fuelLeft = fuel;
        var colonistsLeft = colonists;
        var mineralsLeft = minerals.clone();
        
        do {
            if(fuelLeft <= 0 && mineralsLeft.getMass() == 0 && 
                    colonistsLeft == 0) break;
            
            //chosen randomly
            var task = tasks[Std.random(tasks.length)];
            
            //the amount of cargo that the fleet is trying to load
            var fuelLoad = getCargoAmountValue(task.fuel, task.fuelCapacity, 
                                               Std.int(fuelLeft));
            var colonistLoad = getCargoAmountValue(task.colonists, 
                                                   task.cargoCapacity,
                                                   colonistsLeft);
            
            var mineralLoad = new MineralSet();
            for (minType in Concentrations.MINERAL_TYPES) {
                mineralLoad.setValue(minType, 
                        getCargoAmountValue(task.minerals.get(minType), 
                                            task.cargoCapacity,
                                            mineralsLeft.getValue(minType)));
            }
            
            //now load the cargo, respecting any limits, in the following order:
            //fuel -> colonists -> ironium -> boranium -> germanium.
            var fuelLoaded = Math.min(fuelLoad, task.fleet.getFuelCapacity() 
                                                 - task.fleet.fuel);
            task.fleet.fuel += fuelLoaded;
            fuelLeft -= fuelLoad;
            
            var cargoCapacity = task.cargoCapacity 
                                - task.fleet.colonists 
                                - task.fleet.minerals.getMass();
                                
            //load colonists
            var colonistsLoaded = minInt(colonistLoad, cargoCapacity);
            task.fleet.colonists += colonistsLoaded;
            cargoCapacity -= colonistsLoaded;
            colonistsLeft -= colonistsLoaded;
            
            //load minerals
            for (minType in Concentrations.MINERAL_TYPES) {
                var mineralLoaded = minInt(mineralLoad.getValue(minType), 
                                           cargoCapacity);
                                           
                task.fleet.minerals.setValue(minType, mineralLoaded 
                        + task.fleet.minerals.getValue(minType));
                cargoCapacity -= mineralLoaded;
                mineralsLeft.setValue(minType, mineralsLeft.getValue(minType)
                        - mineralLoaded);
            }
            
            task.fleet.waypoint0 = Task.NoTask;
            tasks.remove(task);
        } while (tasks.length > 0);
        
        return { fuel : fuelLeft, colonists : colonistsLeft };
    }
    
    //defines a min function here because [Math.min] always returns a float
    private static inline function minInt(v1 : Int, v2 : Int) : Int {
        return if (v1 < v2) v1 else v2;
    }
    
    private static function getCargoAmountValue(ca : CargoAmount, 
            targetAmount : Int, left : Int) : Int {
        return minInt(switch(ca) {
            
            case /*CargoAmount.*/All: targetAmount;
            
            case /*CargoAmount.*/None: 0;
            
            case /*CargoAmount.*/Value(v): Std.int(Math.min(v, targetAmount));
            
            case /*CargoAmount.*/Percent(p): Math.ceil(p * targetAmount);
        }, left);
    }
    
    /**
        Performs all of the unload tasks of fleets with a waypoint0 order of 
        unload.
        Note: Fleets can only unload to stars at the momement (packets later).
        For cargo transfer between fleets, one fleet should load from another.
    **/
    public static function doUnloadTasks() {
        var groundCombats = new Array < Array < Attacker >> ();
        for (i in 0...Global.stars.stars.length) groundCombats.push(null);
        
        for (f in Global.fleets) {
            
            switch(f.waypoint0) {
                
                case /*Task.*/Drop(fuel, minerals, colonists, ct):
                    //update target, remove if its null, or the fleet isn't
                    //at the given location.
                    var update = updateCargoTarget(ct);
                    if (update.isNull || 
                        !checkTargetLocation(update.ct, f.location)) {
                        f.waypoint0 = NoTask;
                        continue;
                    }
                    ct = update.ct;
                    
                    switch(ct) {
                        
                        case FleetCargo(f):
                            Global.messages.sendMessage( { 
                                to : Target.PlayerTarget(f.owner), 
                                from : Target.Host, 
                                data : MessageData.Error("You cannot unload " +
                                        "to another fleet. The other fleet " +
                                        "must unload from this fleet.")
                            } );
                            
                        case SalvageCargo(s):
                            Global.messages.sendMessage( { 
                                to : Target.PlayerTarget(f.owner), 
                                from : Target.Host, 
                                data : MessageData.Error("You cannot unload " +
                                        "to salvage.")
                            } );
                            
                        case StarCargo(s):
                            //fuel is ignored here (why would you want to 
                            //unload fuel???)
                            
                            for (minType in Concentrations.MINERAL_TYPES) {
                                var fleetAmount = f.minerals.getValue(minType);
                                var amountRemoved = getCargoAmountValue(
                                        minerals.get(minType), fleetAmount, 
                                        fleetAmount);
                                
                                s.minerals.setValue(minType, 
                                        s.minerals.getValue(minType) 
                                        + amountRemoved);
                                f.minerals.setValue(minType, 
                                        f.minerals.getValue(minType) 
                                        - amountRemoved);
                            }
                            
                            var numColonists = getCargoAmountValue(colonists, 
                                        f.colonists, f.colonists);
                            if(numColonists > 0) {
                                //same owner, so we can just add to the 
                                //existing colonists
                                if (s.owner.id == f.owner.id) {
                                    s.population += numColonists;
                                    f.colonists -= numColonists;
                                } else 
                                //the formatting gets a little weird here as 
                                //the code is pushed against the margin
                                
                                //the star is uninhabited so just send a 
                                //message to the player
                                if (s.owner.id == Global.players.nullPlayer.id)
                                {
                                    Global.messages.sendMessage( 
                                        {to : Target.PlayerTarget(
                                                f.owner), 
                                         from : Target.Host, 
                                         data : MessageData.UninhabitedPopDrop(
                                                f.name, s.name)
                                    } );
                                } else {
                                    //here the player has unloaded pop onto an
                                    //inhabited star so ground combat needs to
                                    //be resolved. (that function also checks
                                    //to see if there is a starbase).
                                    f.colonists -= numColonists;
                                    
                                    var a = { troops : numColonists, 
                                                player : f.owner };
                                    
                                    if (groundCombats[s.id] == null) {
                                        groundCombats[s.id] = [a];
                                    } else {
                                        groundCombats[s.id].push(a);
                                    }
                                    
                                }
                                
                            }
                    }
                
                default:
                    //do nothing
            }
        }
        
        //resolve ground combat
        for (i in 0...groundCombats.length) {
            if (groundCombats[i] == null) continue;
            else {
                GroundCombat.resolveCombat(groundCombats[i], 
                        Global.stars.getStarById(i));
            }
        }
    }
    
    /**
        Returns true if the target specified is actual at the given location.
    **/
    private static function checkTargetLocation(target : CargoTarget, 
                                                location : Vector) : Bool {
        return location.equals(switch(target) {
            
            case FleetCargo(f):
                f.location;
                
            case StarCargo(s):
                s.location;
            
            case SalvageCargo(s):
                s.location;
        });
        
    }
    
    /**
        Updates the cargo target to the latest data (such as salvage decaying,
        battles, production, etc).
        Returns the enum, as well as if the target is null.
    **/
    private static function updateCargoTarget(ct : CargoTarget) : 
            {ct : CargoTarget, isNull : Bool} {
        return switch(ct) {
            
            case FleetCargo(f):
                var newF = Global.fleets.getById(f.id, f.owner.id);
                {ct : FleetCargo(newF), isNull : (newF == null) };
                
            case StarCargo(s):
                var newS = Global.stars.getStarById(s.id);
                //stars should never be null.
                {ct : StarCargo(newS), isNull : false }; 
            
            case SalvageCargo(s):
                var newS = Global.salvage.getById(s.id);
                {ct : SalvageCargo(newS), isNull : (newS == null) };
        }
    }
    
}